﻿using System;
using System.Messaging;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Interop.MSMQ;

namespace PSE.Framework.Messaging
{
    /// <summary>
    /// Creates and manages a connection with a message queue.
    /// </summary>
    public class MsmqSender : Msmq, IDisposable
    {
        #region Attributes
        private MSMQManagement _msmqManagement;
        private MessageQueue _messageQueue;
        private BinaryMessageFormatter _binaryMessageFormatter;
        private DateTime _lastHealthStatusChecked;
        private bool _keepRunning;
        private bool _queueExceedsLimit;
        private int _queuedMessageLimit;
        private long _queuedBytesLimit;
        private int _totalMessageSentPerInterval = 0;
        private int _messagesSentPerSecond;
        private bool _msmqManagementActivated;
        #endregion

        #region Constants
        // Tempo entre os testes que validam se a conexão está OK.
        // Esse tempo não deve ser muito alto, pois é onde se valida 
        // se a fila MSMQ não está mais acessível, após conectado
        private const int TIME_TO_CHECK_CONNECTION = 2000;
        /// <summary>
        /// Identifies a connection failure error.
        /// </summary>
        public const int ERROR_CODE_CONNECTION_FAILURE = -999;
        /// <summary>
        /// Identifies an unexpected connection failure error.
        /// </summary>
        public const int ERROR_CODE_UNEXPECTED_FAILURE_ON_CONNECT = -998;
        /// <summary>
        /// Identifies a queue doesn't exist error.
        /// </summary>
        public const int ERROR_CODE_QUEUE_DOESNT_EXIST = -997;
        /// <summary>
        /// Identifies an unexpected failure on thread running.
        /// </summary>
        public const int ERROR_CODE_UNEXPECTED_FAILURE_ON_THREAD_RUNNING = -994;
        /// <summary>
        /// Identifies an error when creating the msmq management object.
        /// </summary>
        public const int ERROR_CODE_CREATING_MSMQ_MANAGEMENT = -993;
        /// <summary>
        /// Identifies an error when reading the queue's size.
        /// </summary>
        public const int ERROR_CODE_READING_QUEUE_SIZE = -992;
        /// <summary>
        /// Identifies an error when reading the queue's message quantity.
        /// </summary>
        public const int ERROR_CODE_READING_QUEUE_MESSAGE_QUANTITY = -991;
        /// <summary>
        /// Identifies an error when reading the queue's message quantity.
        /// </summary>
        public const int ERROR_CODE_QUEUE_LIMIT_REACH = -990;
        /// <summary>
        /// Identifies an unexpected failure on check health status.
        /// </summary>
        public const int ERROR_CODE_UNEXPECTED_FAILURE_ON_CHECK_HEALTH_STATUS = -989;
        /// <summary>
        /// Identifies an unexpected failure on health status event.
        /// </summary>
        public const int ERROR_CODE_UNEXPECTED_FAILURE_ON_HEALTH_STATUS_EVENT = -988;
        /// <summary>
        /// Identifies an error that MsmqSender object cannot be written.
        /// </summary>
        public const int ERROR_CODE_MSMQ_CANT_WRITE = -987;
        /// <summary>
        /// Identifies an error that MsmqSender object cannot send message.
        /// </summary>
        public const int ERROR_CODE_MSMQ_CANT_SEND = -986;
        /// <summary>
        /// Identifies an unexpected failure on send message.
        /// </summary>
        public const int ERROR_CODE_UNEXPECTED_FAILURE_ON_SEND_MESSAGE = -985;
        /// <summary>
        /// Identifies an error when trying to send a message when disconnected.
        /// </summary>
        public const int ERROR_CODE_TRIED_TO_SEND_WHEN_DISCONNECTED = -984;
        /// <summary>
        /// Identifies an error when trying to send a message when queue's limit exceeded.
        /// </summary>
        public const int ERROR_CODE_TRIED_TO_SEND_WHEN_QUEUE_LIMIT_EXCEEDED = -983;
        /// <summary>
        /// Identifies a msmq failure on purge queue.
        /// </summary>
        public const int ERROR_CODE_MSMQ_FAILURE_ON_PURGE_QUEUE = -981;
        /// <summary>
        /// Identifies an unexpected failure on purge queue.
        /// </summary>
        public const int ERROR_CODE_UNEXPECTED_FAILURE_ON_PURGE_QUEUE = -980;
        /// <summary>
        /// Identifies an error when purging queue when not connected.
        /// </summary>
        public const int ERROR_CODE_PURGING_QUEUE_WHEN_NOT_CONNECTED = -979;
        #endregion

        #region Delegates
        /// <summary>
        ///  Represents the method that will handle the MsmqSender.OnHealthStatusChanged event of a MsmqSender object.
        /// </summary>
        /// <param name="MsmqSender">The object itself.</param>
        public delegate void HealthStatusChanged(MsmqSender MsmqSender);
        #endregion

        #region Events
        /// <summary>
        /// Occurs when the PSE.Framework.Messaging.MsmqSender object changes its state.
        /// </summary>
        public event HealthStatusChanged OnHealthStatusChanged;
        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the limit of the queue in bytes. 
        /// </summary>
        public long QueuedBytesLimit
        {
            get { return _queuedBytesLimit; }
            set { _queuedBytesLimit = value; }
        }

        /// <summary>
        /// Gets or sets the limit of the queue in message number. 
        /// </summary>
        public int QueuedMessageLimit
        {
            get { return _queuedMessageLimit; }
            set { _queuedMessageLimit = value; }
        }

        /// <summary>
        /// Gets a value indicating whether the queue exceeds either the limit in bytes or the limit in message number.
        /// </summary>
        public bool QueueExceedsLimit
        {
            get { return _queueExceedsLimit; }
        }

        /// <summary>
        /// Gets the number of messages sent per second by the MsmqSender object.
        /// </summary>
        public int MessagesSentPerSecond
        {
            get { return _messagesSentPerSecond; }
        }

        private bool IsRunning()
        {
            return _keepRunning;
        }

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the PSE.Framework.Messaging.MsmqSender class.
        /// </summary>
        /// <param name="path">The path of the message queue.</param>
        public MsmqSender(string path)
        {
            if (string.IsNullOrEmpty(path))
                throw new ArgumentNullException("path");

            Initialize(path, -1, -1);
        }

        /// <summary>
        /// Initializes a new instance of the PSE.Framework.Messaging.MsmqSender class.
        /// </summary>
        /// <param name="path">The path of the message queue.</param>        
        /// <param name="queueMessageLimit">The maximum number of messages that the queue can enqueue.</param>
        /// <param name="queueBytesLimit">The maximum size in bytes that the queue can store.</param>
        public MsmqSender(string path, int queueMessageLimit, long queueBytesLimit)
        {
            if (string.IsNullOrEmpty(path))
                throw new ArgumentNullException("path");

            if (queueMessageLimit == 0)
                throw new ArgumentOutOfRangeException("Parameter queueMessageLimit must be greater than 0.");

            if (queueBytesLimit == 0)
                throw new ArgumentOutOfRangeException("Parameter queueBytesLimit must be greater than 0.");

            Initialize(path, queueMessageLimit, queueBytesLimit);
        }
        #endregion

        #region Methods
        private void Initialize(string path, int queueMessageLimit, long queueBytesLimit)
        {
            this.Path = path;

            //Incializações diversas            
            _lastHealthStatusChecked = DateTime.Now;
            QueuedMessageLimit = queueMessageLimit * 85 / 100; // Calcula um limite com 15% de margem de erro
            QueuedBytesLimit = queueBytesLimit * 85 / 100; // Calcula um limite com 15% de margem de erro            

            //Dispara a thread que ficará mantendo a conexão com o MSMQ para envio de mensagens
            Thread thr = new Thread(new ThreadStart(Run));
            thr.Name = QueueName;
            thr.Start();
        }
        /// <summary>
        /// 
        /// </summary>
        private void Run()
        {
            _binaryMessageFormatter = new BinaryMessageFormatter();
            _keepRunning = true;

            // Thread ficará rodando até _keepRunning = false
            // _keepRunning só será false na chamada do método Dispose. 
            //_traceWriter.Trace("Iniciou Thread que ficará enviando mensagens...");
            while (_keepRunning)
            {
                try
                {
                    if (!_isConnected) //Na primeira vez não estará conectado, a partir daí, se estiver conectado, não faz nada
                    {
                        if (QueueExists(_queueName)) // Se a fila existir vai se conectar
                        {
                            try
                            {
                                // Se conecta ao MSMQ
                                _messageQueue = new MessageQueue(_path);
                                _isConnected = true;
                                CheckMessageQueueHealthStatus();

                                HandleConnection();
                            }
                            catch (MessageQueueException messageQueueException)
                            {
                                _isConnected = false; //Tem que ser a primeira linha, para os que dependem dessa variável saber o quanto antes                                
                                _lastErrorMessage = "Error when trying to connect to the MSMQ:" + messageQueueException.Message;
                                _lastErrorCode = ERROR_CODE_CONNECTION_FAILURE;

                                HandleError(ERROR_CODE_CONNECTION_FAILURE, messageQueueException);
                                HandleDisconnection();
                            }
                            catch (Exception ex)
                            {
                                _isConnected = false; //Tem que ser a primeira linha, para os que dependem dessa variável saber o quanto antes
                                _lastErrorMessage = "Error not expected when trying to connect to the MSMQ:" + ex.Message;
                                _lastErrorCode = ERROR_CODE_UNEXPECTED_FAILURE_ON_CONNECT;

                                HandleError(ERROR_CODE_UNEXPECTED_FAILURE_ON_CONNECT, ex);
                                HandleDisconnection();
                            }
                        }
                        else // Se a fila não existir
                        {
                            _isConnected = false; //Tem que ser a primeira linha, para os que dependem dessa variável saber o quanto antes
                            _lastErrorMessage = string.Format("Queue {0} doesn't exist or cannot be accessed.", _path);
                            _lastErrorCode = ERROR_CODE_QUEUE_DOESNT_EXIST;

                            HandleError(ERROR_CODE_QUEUE_DOESNT_EXIST, new Exception(_lastErrorMessage));
                            HandleDisconnection();
                        }
                    }
                    else // Se o controle de estado diz que está conectado
                    {
                        // Confere se a fila existe para manter esse estado
                        try
                        {
                            if (!QueueExists(_queueName))
                            {
                                _isConnected = false; //Tem que ser a primeira linha, para os que dependem dessa variável saber o quanto antes
                                _lastErrorMessage = "Queue doesn't exist.";
                                _lastErrorCode = ERROR_CODE_QUEUE_DOESNT_EXIST;

                                HandleError(ERROR_CODE_QUEUE_DOESNT_EXIST, new Exception(string.Format("Queue [{0}] doesn't exist anymore!", _path)));
                                HandleDisconnection();
                            }
                        }
                        catch (Exception ex)
                        {
                            _isConnected = false; //Tem que ser a primeira linha, para os que dependem dessa variável saber o quanto antes
                            _lastErrorMessage = "Error when trying to verify if the queue exists.";
                            _lastErrorCode = ERROR_CODE_QUEUE_DOESNT_EXIST;

                            HandleError(ERROR_CODE_QUEUE_DOESNT_EXIST, ex);
                            HandleDisconnection();
                        }

                        CheckMessageQueueHealthStatus();
                    }
                }
                catch (Exception ex)
                {
                    _isConnected = false; //Tem que ser a primeira linha, para os que dependem dessa variável saber o quanto antes
                    _lastErrorMessage = "It's impossible to detect error...";
                    _lastErrorCode = ERROR_CODE_UNEXPECTED_FAILURE_ON_THREAD_RUNNING;

                    HandleError(ERROR_CODE_UNEXPECTED_FAILURE_ON_THREAD_RUNNING, ex);
                    HandleDisconnection();
                }

                // Dá um tempo para checar a conexão e se for o caso tentar se conectar novamente
                PSE.Framework.Threading.Sleeper.Sleep(TIME_TO_CHECK_CONNECTION, IsRunning);
            }
        }

        private bool StartMsmqManagement()
        {
            bool ret;
            try
            {
                _msmqManagement = null;
                _msmqManagement = new MSMQManagement();
                _msmqManagement.Init(ref _machineNameObject, ref _queueNameObject, ref _formatNameObject);
                ret = true;
            }
            catch (Exception ex)
            {
                _lastErrorMessage = "Error when trying to create Interop.MSMQManagement object.";
                _lastErrorCode = ERROR_CODE_CREATING_MSMQ_MANAGEMENT;

                ret = false;

                HandleError(ERROR_CODE_CREATING_MSMQ_MANAGEMENT, ex);
            }

            return ret;
        }

        private void CheckMessageQueueHealthStatus()
        {
            try
            {
                // Reativa o objeto de gerenciamento do MSMQ caso esse seja desativado, 
                if (!_msmqManagementActivated)
                {
                    _msmqManagementActivated = StartMsmqManagement();
                }

                // Lê tamanho da fila em Bytes
                try
                {
                    if (_msmqManagementActivated)
                    {
                        _queueSizeBytes = long.Parse(_msmqManagement.BytesInQueue.ToString());
                    }
                }
                catch (Exception ex)
                {
                    // Se der erro, é porque o objeto de gerenciamento do MSMQ deu problema
                    _queueSizeBytes = -1;
                    _msmqManagementActivated = false; // Define o objeto de gerenciamento do MSMQ  como desativado

                    _lastErrorMessage = "It was not possible to read the queue's size.";
                    _lastErrorCode = ERROR_CODE_READING_QUEUE_SIZE;

                    HandleError(ERROR_CODE_READING_QUEUE_SIZE, ex);
                }

                // Lê tamanho número de mensagens da fila
                try
                {
                    if (_msmqManagementActivated)
                    {
                        _queuedMessages = _msmqManagement.MessageCount;
                    }
                }
                catch (Exception ex)
                {
                    // Se der erro, é porque o objeto de gerenciamento do MSMQ deu problema
                    _queuedMessages = -1;
                    _msmqManagementActivated = false; // Define o objeto de gerenciamento do MSMQ  como desativado

                    _lastErrorMessage = "It was not possible to read the queue's message number.";
                    _lastErrorCode = ERROR_CODE_READING_QUEUE_MESSAGE_QUANTITY;

                    HandleError(ERROR_CODE_READING_QUEUE_MESSAGE_QUANTITY, ex);
                }

                // Se os limites não foram definidos, não fazer a validação.
                if (QueuedBytesLimit != -1 && QueuedMessageLimit != -1)
                {
                    // Se a fila excedeu qualquer um dos tamanhos pré-configurados, define que não é mais para mandar mensagens
                    if (QueueSizeBytes > QueuedBytesLimit || QueuedMessages > QueuedMessageLimit)
                    {
                        _queueExceedsLimit = true;

                        _lastErrorMessage = "Queue has exceeded the defined limits.";
                        _lastErrorCode = ERROR_CODE_QUEUE_LIMIT_REACH;

                        HandleError(ERROR_CODE_QUEUE_LIMIT_REACH, new Exception(_lastErrorMessage));
                    }
                    else
                    {
                        //_traceWriterHealthStatus.Trace("SUCESSO: Fila NÃO excedeu limite!", TraceType.Optional);
                        _queueExceedsLimit = false;
                    }
                }
                // Calcula o número de mensagens por segundo
                _messagesSentPerSecond = CalculateMessageNumberPerSecond();

                // Dispara o evento com os dados de estatística do componente
                ThrowEventOnHealthStatusChanged();
            }
            catch (Exception ex)
            {
                _lastErrorMessage = "Error not identified on CheckMessageQueueHealthStatus() method.";
                _lastErrorCode = ERROR_CODE_UNEXPECTED_FAILURE_ON_CHECK_HEALTH_STATUS;

                HandleError(ERROR_CODE_UNEXPECTED_FAILURE_ON_CHECK_HEALTH_STATUS, ex);
            }
        }

        private int CalculateMessageNumberPerSecond()
        {
            int seconds;

            // Calcula o número de segundos entre a última coleta e a coleta atual
            seconds = DateTime.Now.Subtract(_lastHealthStatusChecked).Seconds;

            if (seconds == 0)
                seconds = TIME_TO_CHECK_CONNECTION / 1000;

            if (_totalMessageSentPerInterval >= seconds)
                _messagesSentPerSecond = _totalMessageSentPerInterval / seconds;
            else
                _messagesSentPerSecond = _totalMessageSentPerInterval;

            // Zera o contador que será usado na próxima coleta
            // Esse contador é incrementado dentro do Send(Message message)
            // a cada mensagem enviada
            _totalMessageSentPerInterval = 0;

            // Define o horário dessa verificação, para ser usada na próxima chamada da função e calcular os segundos entre uma chamada e outra
            _lastHealthStatusChecked = DateTime.Now;

            return _messagesSentPerSecond;
        }

        private void ThrowEventOnHealthStatusChanged()
        {
            try
            {
                if (OnHealthStatusChanged != null) // Caso alguém tenha assinado o evento
                    OnHealthStatusChanged(this);
            }
            catch (Exception ex)
            {
                _lastErrorMessage = "Error when throwing OnHealthStatusChanged() event.";
                _lastErrorCode = ERROR_CODE_UNEXPECTED_FAILURE_ON_HEALTH_STATUS_EVENT;

                HandleError(ERROR_CODE_UNEXPECTED_FAILURE_ON_HEALTH_STATUS_EVENT, ex);
            }
        }

        /// <summary>
        /// Sends a message to the queue.
        /// </summary>
        /// <param name="message">The message to be sent.</param>
        /// <returns>Returns a value indicating whether the sending was successful.</returns>
        public bool Send(Message message)
        {
            if (message == null)
                throw new ArgumentNullException("message");

            bool ret = false;

            if (_isConnected && !_queueExceedsLimit) // Só manda mensagem se estiver conectado ou a fila não tenha excedido o limite
            {
                try
                {
                    if (_messageQueue.CanWrite) // Verifica se o Stream está pronto para escrita
                    {

                        message.Formatter = _binaryMessageFormatter;
                        _messageQueue.Send(message); //Manda a mensagem                        

                        // Atualiza o contator que será usado para calcular o número de mensagens por segundo
                        _totalMessageSentPerInterval++;

                        ret = true;
                    }
                    else
                    {
                        _lastErrorMessage = "Msmq's CanWrite Status doesn't allowed message to be sent.";
                        _lastErrorCode = ERROR_CODE_MSMQ_CANT_WRITE;

                        ret = false;
                    }
                }
                catch (MessageQueueException ex)
                {
                    _isConnected = false; //Tem que ser a primeira linha, para os que dependem dessa variável saber o quanto antes

                    _lastErrorMessage = "Error when trying to send message.";
                    _lastErrorCode = ERROR_CODE_MSMQ_CANT_SEND;
                    ret = false;

                    HandleError(ERROR_CODE_MSMQ_CANT_SEND, ex);
                }
                catch (Exception ex)
                {
                    _isConnected = false; //Tem que ser a primeira linha, para os que dependem dessa variável saber o quanto antes

                    _lastErrorMessage = "Error not identified when trying to send message.";
                    _lastErrorCode = ERROR_CODE_UNEXPECTED_FAILURE_ON_SEND_MESSAGE;

                    ret = false;

                    HandleError(ERROR_CODE_UNEXPECTED_FAILURE_ON_SEND_MESSAGE, ex);
                }
            }
            else
            {
                if (!_isConnected)
                {
                    _lastErrorMessage = "It's tried to send a message with MSMQ object disconnected.";
                    _lastErrorCode = ERROR_CODE_TRIED_TO_SEND_WHEN_DISCONNECTED;
                }
                else if (_queueExceedsLimit)
                {
                    _isConnected = false;
                    _lastErrorMessage = "It's tried to send a message but the MSMQ's size limit is exceeded. \\r\\n";
                    _lastErrorCode = ERROR_CODE_TRIED_TO_SEND_WHEN_QUEUE_LIMIT_EXCEEDED;

                    HandleError(ERROR_CODE_TRIED_TO_SEND_WHEN_QUEUE_LIMIT_EXCEEDED, new Exception(_lastErrorMessage));
                    HandleDisconnection();
                }
                else
                {
                    _isConnected = false;
                    _lastErrorMessage = "It's tried to send a message but a not catalogued error has occurred.";
                    _lastErrorCode = ERROR_CODE_UNEXPECTED_FAILURE_ON_SEND_MESSAGE;

                    HandleDisconnection();
                }
                ret = false;
            }

            return ret;
        }

        /// <summary>
        /// Releases all resources.
        /// </summary>
        public void Dispose()
        {
            // Tem que ser as duas primeiras linhas, para os que dependem dessa variável saber o quanto antes
            // _keepRunning = false tem que vir antes de _connected = false
            // caso contrario corre-se o risco do status mudar para conectado por a thread ainda estar aberta.
            _keepRunning = false; // Determina que as Thread abertas podem ser fechadas...
            _isConnected = false; // Determina que não estamos mais conectados...            

            try
            {
                if (_messageQueue != null)
                    _messageQueue.Close();

                if (_msmqManagement != null)
                    Marshal.ReleaseComObject(_msmqManagement);

                _messageQueue = null;
                _msmqManagement = null;
            }
            catch (Exception ex)
            {
                StringBuilder error = new StringBuilder();
                error.Append("Error when disposing the object... \\n");
                error.Append(string.Concat(ex.Message, " \\n"));
                error.Append(string.Concat(ex.StackTrace, " \\n"));

                HandleError(0, new Exception(error.ToString()));
            }

            HandleDisconnection();
        }

        /// <summary>
        /// Deletes all the messages contained in the queue.
        /// </summary>
        /// <returns>Returns a value indicating whether the deletion was successful.</returns>
        public bool Purge()
        {
            bool ret = false;

            if (_isConnected) // Só manda mensagem se estiver conectado ou a fila não tenha excedido o limite
            {
                try
                {
                    if (_messageQueue != null)
                        _messageQueue.Purge();

                    ret = true;
                }
                catch (MessageQueueException messageQueueException)
                {
                    _isConnected = false; //Tem que ser a primeira linha, para os que dependem dessa variável saber o quanto antes

                    _lastErrorMessage = "It's impossible to clean the queue due to failure on MSMQ object.";
                    _lastErrorCode = ERROR_CODE_MSMQ_FAILURE_ON_PURGE_QUEUE;

                    ret = false;

                    HandleError(ERROR_CODE_MSMQ_FAILURE_ON_PURGE_QUEUE, messageQueueException);
                    HandleDisconnection();
                }
                catch (Exception exception)
                {
                    _isConnected = false; //Tem que ser a primeira linha, para os que dependem dessa variável saber o quanto antes

                    _lastErrorMessage = "Error not identified when cleaning the queue.";
                    _lastErrorCode = ERROR_CODE_UNEXPECTED_FAILURE_ON_PURGE_QUEUE;

                    ret = false;

                    HandleError(ERROR_CODE_UNEXPECTED_FAILURE_ON_PURGE_QUEUE, exception);
                    HandleDisconnection();
                }
            }

            return ret;
        }

        #endregion
    }
}
